Skip to content

feat(models): surface the API's agency-filter diagnostics on PaginatedResponse - #55

Open
makegov-mark[bot] wants to merge 1 commit into
mainfrom
fix/2692-agency-filter-meta
Open

feat(models): surface the API's agency-filter diagnostics on PaginatedResponse#55
makegov-mark[bot] wants to merge 1 commit into
mainfrom
fix/2692-agency-filter-meta

Conversation

@makegov-mark

@makegov-mark makegov-mark Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What

PaginatedResponse gains meta, carrying the response-level metadata the API returns, plus three accessors over it.

Agency filter values resolve fuzzily. Two things can go wrong, and until now neither was visible to an SDK user:

  1. A token matches nothing and is dropped. The response reflects a filter that was never applied.
  2. A token matches the wrong organization and scopes the query to that subtree. Plausible, confident, wrong — and nothing was dropped, so there is no gap to notice.

Both are indistinguishable from "no such records exist". The API reports both in a meta block, but the SDK built PaginatedResponse key-by-key (count, next, previous, results), so meta was read by nobody and discarded. SDK users were the one group that could not see the diagnostics at all.

API

response = client.list_contracts(awarding_agency="HUD|HUDD")

response.unresolved_agency_tokens
# {'awarding_agency': ['HUDD']}     — matched nothing, per filter

response.resolved_agencies["awarding_agency"]
# [{'name': 'Department of Housing and Urban Development', 'cgac': '086', ...}]

response.agency_warnings
# ["Agency filter 'awarding_agency': 'HUDD' did not match any organization and was ignored."]

resolved_agencies is the one worth understanding. It is not a nicer spelling of the dropped-token check — it is the only client-side signal for case 2 above, where nothing is dropped and the only tell is that the resolved name is not the organization you meant.

Notes

  • Populated at all 48 PaginatedResponse construction sites, so it is uniform rather than available on whichever methods happened to get it.
  • All three accessors return empty rather than raising when meta is absent (the common case) or malformed. meta is server-controlled; a shape change on the API side must not break a caller's loop. Covered by a test.
  • page_metadata is documented as always None. The API has never emitted a page_metadata key, so the field has only ever read something that does not exist. Retained rather than removed, since deleting an attribute breaks anyone referencing it — worth a separate deprecation decision.
  • No change was needed for the 400 path. A fully-unresolvable agency filter already raises TangoValidationError through the existing error-key handler. It is new behavior for list_subawards(), list_opportunities(), list_notices() and list_vehicles(), which previously returned an empty page, so there is now a test pinning it.

Testing

345 pass, mypy clean, no new ruff findings (the 20 in the repo are pre-existing and none are on lines this touches). Seven new tests cover: meta passthrough, dropped tokens across multiple filters, the wrong-organization case, warnings, absent meta, malformed meta, and the 400.

…dResponse

Agency values resolve fuzzily. A token can match nothing and be dropped, or match an organization the caller never intended and quietly scope the query to that subtree. Both look identical to "no such records exist" from the client side.

The API reports both in a response-level `meta` block, but the SDK built `PaginatedResponse` key-by-key — `count`, `next`, `previous`, `results` — so `meta` was read by nobody and discarded. SDK users were the one group that could not see the diagnostics at all.

`PaginatedResponse.meta` now carries it, populated at all 48 construction sites, with three accessors over the raw dict:

- `unresolved_agency_tokens` — tokens that matched nothing, keyed by filter, for failing loudly in a pipeline.
- `resolved_agencies` — the organization each token *did* match. This is the one that catches the wrong-organization case: nothing is dropped there, so an unresolved-token check cannot detect it. Comparing the resolved `name` is the only client-side signal.
- `agency_warnings` — the API's human-readable notes.

All three return empty rather than raising when `meta` is absent (most responses) or malformed, since `meta` is server-controlled and a shape change must not break a caller's loop.

Also documents `page_metadata` as always `None`: the API has never emitted a `page_metadata` key, so the field has only ever read something that does not exist. Retained so existing attribute access keeps working.

No change was needed for the 400 path — a fully-unresolvable agency filter already raises `TangoValidationError` via the existing `error`-key handler — but it is new behavior for `list_subawards()`, `list_opportunities()`, `list_notices()` and `list_vehicles()`, which previously returned an empty page. Covered by a test so the contract is pinned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant